// Package tls_test provides tests for the tls package.
//
// This file contains tests for the TLS configuration utilities, including:
// - Creating TLS configs from certificate and key data
// - Loading TLS configs from certificate and key files
// - Handling various error conditions such as missing or invalid certificates
//
// The test suite includes a helper function for generating self-signed certificates
// for testing purposes. This allows testing TLS functionality without requiring
// external certificate files.
package tls

import (
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

// generateTestCertificate creates a self-signed certificate and key for testing purposes.
//
// This helper function generates an RSA key pair and a self-signed X.509 certificate
// with the following properties:
// - 2048-bit RSA key
// - Valid for 1 hour from generation time
// - Subject is "CN=localhost"
// - Includes a SubjectAltName extension for "localhost"
// - Appropriate for server authentication use
//
// The function returns PEM-encoded certificate and private key as byte slices,
// ready to be used with the New function.
//
// This is useful for testing TLS functionality without requiring external certificate files.
// Note that certificates generated by this function should never be used in production.
func generateTestCertificate(t *testing.T) (certPEM []byte, keyPEM []byte) {
	t.Helper()

	// Generate a private key
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	require.NoError(t, err, "Failed to generate private key")

	// Create a template for the certificate
	serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
	require.NoError(t, err, "Failed to generate serial number")

	notBefore := time.Now()
	notAfter := notBefore.Add(time.Hour) // Valid for an hour

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"Unkey Test"},
			CommonName:   "localhost",
		},
		NotBefore:             notBefore,
		NotAfter:              notAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		DNSNames:              []string{"localhost"},
	}

	// Create the certificate
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
	require.NoError(t, err, "Failed to create certificate")

	// Encode the certificate to PEM
	certBuffer := &bytes.Buffer{}
	err = pem.Encode(certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	require.NoError(t, err, "Failed to encode certificate to PEM")

	// Encode the private key to PEM
	keyBuffer := &bytes.Buffer{}
	err = pem.Encode(keyBuffer, &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
	})
	require.NoError(t, err, "Failed to encode private key to PEM")

	return certBuffer.Bytes(), keyBuffer.Bytes()
}

// TestNewWithValidCertificateAndKey verifies that the New function correctly
// creates a TLS configuration when provided with valid certificate and key data.
// It checks that the resulting config has the expected certificates and security settings.
func TestNewWithValidCertificateAndKey(t *testing.T) {
	certPEM, keyPEM := generateTestCertificate(t)

	tlsConfig, err := New(certPEM, keyPEM)

	require.NoError(t, err)
	// A valid TLS config should have certificates
	require.NotEmpty(t, tlsConfig.Certificates)
	// Check TLS version is at least 1.3
	require.Equal(t, uint16(0x0304), tlsConfig.MinVersion) // TLS 1.3
}

// TestNewWithEmptyCertificate verifies that the New function returns an appropriate
// error when provided with an empty certificate but valid key.
func TestNewWithEmptyCertificate(t *testing.T) {
	_, keyPEM := generateTestCertificate(t)

	tlsConfig, err := New([]byte{}, keyPEM)

	require.Error(t, err)
	require.Contains(t, err.Error(), "TLS certificate must not be empty")
	require.Nil(t, tlsConfig)
}

// TestNewWithEmptyKey verifies that the New function returns an appropriate
// error when provided with a valid certificate but an empty key.
func TestNewWithEmptyKey(t *testing.T) {
	certPEM, _ := generateTestCertificate(t)

	tlsConfig, err := New(certPEM, []byte{})

	require.Error(t, err)
	require.Contains(t, err.Error(), "TLS key must not be empty")
	require.Nil(t, tlsConfig)
}

// TestNewWithInvalidCertificate verifies that the New function returns an appropriate
// error when provided with invalid certificate data that cannot be parsed.
func TestNewWithInvalidCertificate(t *testing.T) {
	_, keyPEM := generateTestCertificate(t)

	tlsConfig, err := New([]byte("not a valid certificate"), keyPEM)

	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to parse TLS certificate")
	require.Nil(t, tlsConfig)
}

// TestNewWithInvalidKey verifies that the New function returns an appropriate
// error when provided with invalid key data that cannot be parsed.
func TestNewWithInvalidKey(t *testing.T) {
	certPEM, _ := generateTestCertificate(t)

	tlsConfig, err := New(certPEM, []byte("not a valid key"))

	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to parse TLS certificate")
	require.Nil(t, tlsConfig)
}

// TestNewFromFilesWithValidFiles verifies that the NewFromFiles function correctly
// loads certificate and key data from files and creates a valid TLS configuration.
// It creates temporary files with valid certificate and key data, then ensures
// the resulting config has the expected certificates and security settings.
func TestNewFromFilesWithValidFiles(t *testing.T) {
	// Generate test certificate and key
	certPEM, keyPEM := generateTestCertificate(t)

	// Create temp directory for test files
	tempDir := t.TempDir()

	// Create temp certificate file
	certPath := filepath.Join(tempDir, "cert.pem")
	err := os.WriteFile(certPath, certPEM, 0600)
	require.NoError(t, err, "Failed to write certificate to temp file")

	// Create temp key file
	keyPath := filepath.Join(tempDir, "key.pem")
	err = os.WriteFile(keyPath, keyPEM, 0600)
	require.NoError(t, err, "Failed to write key to temp file")

	tlsConfig, err := NewFromFiles(certPath, keyPath)
	require.NoError(t, err)
	// A valid TLS config should have certificates
	require.NotEmpty(t, tlsConfig.Certificates)
	// Check TLS version is at least 1.3
	require.Equal(t, uint16(0x0304), tlsConfig.MinVersion) // TLS 1.3
}

// TestNewFromFilesWithNonExistentCertificate verifies that the NewFromFiles function
// returns an appropriate error when the certificate file does not exist.
func TestNewFromFilesWithNonExistentCertificate(t *testing.T) {
	// Generate test certificate and key
	_, keyPEM := generateTestCertificate(t)

	// Create temp directory for test files
	tempDir := t.TempDir()

	// Create temp key file
	keyPath := filepath.Join(tempDir, "key.pem")
	err := os.WriteFile(keyPath, keyPEM, 0600)
	require.NoError(t, err, "Failed to write key to temp file")

	_, err = NewFromFiles("/non/existent/cert.pem", keyPath)
	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to read certificate file")
}

// TestNewFromFilesWithNonExistentKey verifies that the NewFromFiles function
// returns an appropriate error when the key file does not exist.
func TestNewFromFilesWithNonExistentKey(t *testing.T) {
	// Generate test certificate and key
	certPEM, _ := generateTestCertificate(t)

	// Create temp directory for test files
	tempDir := t.TempDir()

	// Create temp certificate file
	certPath := filepath.Join(tempDir, "cert.pem")
	err := os.WriteFile(certPath, certPEM, 0600)
	require.NoError(t, err, "Failed to write certificate to temp file")

	_, err = NewFromFiles(certPath, "/non/existent/key.pem")
	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to read key file")
}

// TestNewFromFilesWithInvalidCertificateContent verifies that the NewFromFiles function
// returns an appropriate error when the certificate file contains invalid data.
// It creates a temporary file with non-certificate content to simulate this condition.
func TestNewFromFilesWithInvalidCertificateContent(t *testing.T) {
	// Generate test certificate and key
	_, keyPEM := generateTestCertificate(t)

	// Create temp directory for test files
	tempDir := t.TempDir()

	// Create invalid certificate file
	invalidCertPath := filepath.Join(tempDir, "invalid-cert.pem")
	err := os.WriteFile(invalidCertPath, []byte("not a certificate"), 0600)
	require.NoError(t, err)

	// Create temp key file
	keyPath := filepath.Join(tempDir, "key.pem")
	err = os.WriteFile(keyPath, keyPEM, 0600)
	require.NoError(t, err, "Failed to write key to temp file")

	_, err = NewFromFiles(invalidCertPath, keyPath)
	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to parse TLS certificate")
}

// TestNewFromFilesWithInvalidKeyContent verifies that the NewFromFiles function
// returns an appropriate error when the key file contains invalid data.
// It creates a temporary file with non-key content to simulate this condition.
func TestNewFromFilesWithInvalidKeyContent(t *testing.T) {
	// Generate test certificate and key
	certPEM, _ := generateTestCertificate(t)

	// Create temp directory for test files
	tempDir := t.TempDir()

	// Create temp certificate file
	certPath := filepath.Join(tempDir, "cert.pem")
	err := os.WriteFile(certPath, certPEM, 0600)
	require.NoError(t, err, "Failed to write certificate to temp file")

	// Create invalid key file
	invalidKeyPath := filepath.Join(tempDir, "invalid-key.pem")
	err = os.WriteFile(invalidKeyPath, []byte("not a key"), 0600)
	require.NoError(t, err)

	_, err = NewFromFiles(certPath, invalidKeyPath)
	require.Error(t, err)
	require.Contains(t, err.Error(), "failed to parse TLS certificate")
}
